<?php
/**
 * SecureHashing
 * Secure hashing of passwords
 * @author Christian Kruse <cjk@wwwtech.de>
 * @package securehashing
 * @version 1.0
 */

/*
 * Open Source Initiative OSI - The MIT License:Licensing
 *
 * The MIT License
 *
 * Copyright (c) 2009 Christian Kruse <cjk@wwwtech.de>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * Class thrown on fatal errors
 * @author Christian Kruse <cjk@wwwtech.de>
 * @package securehashing
 */
class SecureHashing_Exception extends Exception {
}

/**
 * Interface for RNG (random number generator) providers
 * @author Christian Kruse <cjk@wwwtech.de>
 * @package securehashing
 */
interface SecureHashing_RNGProvider {
  /**
   * Get a number of random bytes
   * @param integer $length The number of random bytes to generate
   * @return string The generated random bytes as a string
   */
  public function get_random_bytes($length);

  /**
   * Get a random number
   * @param integer $min The minimum value for the generated number
   * @param integer $max The maximum value for the generated number
   * @return integer The generated random number
   */
  public function get_random_num($min=0,$max=NULL);
}

/**
 * Interface for hashing providers
 * @author Christian Kruse <cjk@wwwtech.de>
 * @package securehashing
 */
interface SecureHashing_HashProvider {
  public function prefix();
  public function hash($msg);
}

/**
 * The default RNG provider; it uses openssl_random_pseudo_bytes if available and falls back to rand()
 * @author Christian Kruse <cjk@wwwtech.de>
 * @package securehashing
 */
class SecureHashing_DefaultRNGProvider implements SecureHashing_RNGProvider {

  /**
   * Return a sequence of random bytes, generated by openssl_random_pseudo_bytes if available, generated by rand() if not
   * @param integer $length The number of bytes to generate
   * @return string The generated bytes as a string
   */
  public function get_random_bytes($length) {
    if(function_exists('openssl_random_pseudo_bytes')) {
      $strong = true;
      return openssl_random_pseudo_bytes($length,$strong);
    }

    $bytes = "";
    for($i=0;$i<$length;++$i) $bytes .= chr(rand(0,255));
    return $bytes;
  }

  /**
   * Get a random number generated by openssl_random_pseudo_bytes if available, generated by rand() if not
   * @param integer $min The minimum value of the number
   * @param integer $max The maximum value of the number; if not set or 0, it will extend to 0x7FFFFFFF
   * @return integer The random number
   */
  public function get_random_num($min=0,$max=NULL) {
    if(function_exists('openssl_random_pseudo_bytes')) {
      if($max == 0) $max = 0x7FFFFFFF;

      $strong = true;
      $x = $max - $min;
      if($x == 0) $x = $max;

      $num = (int)array_shift(unpack("l",openssl_random_pseudo_bytes(4,$strong))) & 0x7FFFFFFF;

      return $min + ($num % $x);
    }

    if($max == 0) $max = getrandmax();
    return rand($min,$max);
  }
}

/**
 * Class for generating SHA512 hashes; uses $1 as prefix
 * @author Christian Kruse <cjk@wwwtech.de>
 * @package securehashing
 */
class SecureHashing_HashProvider_SHA512 implements SecureHashing_HashProvider {
  /**
   * Return the prefix used to identify the hashing algorithm
   * @return string Returns always $1
   */
  public function prefix() {
    return '$1';
  }

  /**
   * Hash a value with SHA512
   * @param mixed $msg The value to hash
   * @return string The hash value
   */
  public function hash($msg) {
    return hash('sha512',$msg,true);
  }
}

/**
 * Class for generating secure password hashes and random (optional phonetic) passwords
 * @author Christian Kruse <cjk@wwwtech.de>
 * @package securehashing
 */
class SecureHashing {
  static protected $hash_providers = Array();
  static protected $default_hash_provider = '';
  static protected $rng_provider = NULL;

  /**
   * Register a new hashing algorithm
   * @param SecureHashing_HashProvider $obj The provider object. Has to implement the SecureHashing_HashProvider interface
   * @return The object itself on success, false if prefix already exists
   */
  public static function register_hash_provider(SecureHashing_HashProvider $obj) {
    if(!isset(self::$hash_providers[$obj->prefix()])) {
      self::$hash_providers[$obj->prefix()] = $obj;
      return $obj;
    }

    return false;
  }

  /**
   * Set the default hashing provider
   * @param SecureHashing_HashProvider $obj The provider object. Has to implement the SecureHashing_HashProvider interface
   * @return The provider object itself if hashing function could be identified, false otherwise
   */
  public static function set_default_hash_provider(SecureHashing_HashProvider $obj) {
    if(isset(self::$hash_providers[$obj->prefix()])) {
      self::$default_hash_provider = $obj->prefix();
      return $obj;
    }

    return false;
  }

  /**
   * Set the provider used to generate random numbers
   * @param SecureHashing_RNGProvider $obj The provider object. Has to implement the SecureHashing_RNGProvider interface
   * @return Returns always true
   */
  public static function set_rng_provider(SecureHashing_RNGProvider $obj) {
    self::$rng_provider = $obj;
    return true;
  }

  /**
   * Wrapper method internally used to hash a string; simply looks up the algorithm by prefix and calls the function
   * @param string $prefix The prefix identifying the algorithm
   * @param string $str the string to hash
   * @return Returns the Hashed string
   * @throws SecureHashing_Exception when hash function identified by $prefix not found
   */
  protected function hash_str($prefix,$str) {
    if(isset(self::$hash_providers[$prefix])) return self::$hash_providers[$prefix]->hash($str);

    throw new SecureHashing_Exception("Error: hash func identified by $prefix not found!");
  }

  /**
   * Use this method to hash a password to storage representation (which, in fact, is $prefix.$hash.$salt.$iterations)
   * @param string $pw The password to hash
   * @return The hash ready for storage
   */
  public function get_storage_representation($pw) {
    list($prefix,$iterations,$salt,$hsh) = $this->hash($pw);

    return $prefix.$hsh.$salt.pack("V",$iterations);
  }

  /**
   * Use this method to compare a password and a hash (in storage representation)
   * @param string $pw The password
   * @param string $hsh The password hash
   * @return true if password and hashed password are equal, false otherwise
   */
  public function compare($pw,$hsh) {
    $niterations = unpack("V",substr($hsh,strlen($hsh)-4));
    $niterations = array_shift($niterations);

    $prefix = substr($hsh,0,2);
    $hsh = substr($hsh,2,strlen($hsh)-6);
    $salt = substr($hsh,strlen($hsh)-16);

    list($prefix,$iterations,$salt,$new_hsh) = $this->hash($pw,$prefix,$salt,$niterations);
    return $new_hsh.$salt == $hsh;
  }

  /**
   * Method to hash a password
   * This method hashes a password n times; n highly depends on the machine, it hashes 'till 1 second has gone by. This is really strict randomness, since it depends on a LOT of external parameters how often a password will be hashed.
   * @param string $pw The password to hash
   * @param string $prefix The prefix of the hashing function, default hashing function used if $prefix is NULL
   * @param string $salt The salt, 16 bytes long. It will get filled up with random bytes if to short or NULL
   * @param integer $niterations When specified (e.g. not NULL or <= 0) the password will get hashed $niteration times
   * @return array of prefix, iterations, salt and hash (in this order)
   */
  public function hash($pw,$prefix=null,$salt=null,$niterations=null) {
    if($salt !== NULL && !empty($salt)) $salt = substr($salt,strlen($salt)-16);
    if($salt === NULL) $salt = "";

    if(strlen($salt) < 16) $salt .= self::$rng_provider->get_random_bytes(16-strlen($salt)); # we fill up the salt to get 16 bytes

    if($prefix === NULL) $prefix = self::$default_hash_provider;

    $iterations = 0;
    $hsh = "";

    if($niterations === NULL || $niterations <= 0) {
      $now = time();

      $hsh = $this->hash_str($prefix,$pw . $salt);
      $iterations = 1;

      while(time() - $now < 1) {
        $hsh = $this->hash_str($prefix,$hsh . $salt);
        $iterations += 1;
      }
    }
    else {
      $hsh = $pw;
      $iterations = $niterations;
      for($i=0;$i<$niterations;++$i) $hsh = $this->hash_str($prefix,$hsh.$salt);
    }

    return Array($prefix,$iterations,$salt,$hsh);
  }


  protected static $RAND_PASSWD_CHARS = Array(
    '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J',
    'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d',
    'e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x',
    'y','z','(',')','{','}','/','!','"','$','%','&','=','?','+','*','#'
  );

  /**
   * Register new characters for password generation
   * @param string $char Either an array of characters (advised if multibyte characters are used) or a string of characters (each byte is treated as one character!)
   * @return void
   */
  public static function add_chars($char) {
    if(!is_array($char)) $char = preg_split('//',$char);

    foreach($char as $c) {
      if(!in_array($c,$RAND_PASSWD_CHARS)) $RAND_PASSWD_CHARS[] = $c;
    }
  }

  /**
   * Method to generate random passwords
   * @param integer $length The length of the password, 12 by default
   * @return The generated password
   */
  public function random($length=12) {
    $pw = "";
    $nr_chars = count(self::$RAND_PASSWD_CHARS);

    $rand = self::$rng_provider->get_random_bytes($length);

    for($i=0;$i<$length;++$i) $pw .= self::$RAND_PASSWD_CHARS[ord($rand[$i])%$nr_chars];

    return $pw;
  }

  /**
   * Use a digit in generated phonemic password
   * @var integer
   */
  const DIGIT = 1;

  /**
   * Use an uppercase character in generated phonemic password
   * @var integer
   */
  const UPPER = 2;

  /**
   * Use an extra character ('(',')','{','}','/','!','"','$','%','&','=','?','+','*','#') in generated phonemic passwords
   * @var integer
   */
  const EXTRA = 4;

  /**
   * Used internally to identify vowels
   * @var integer
   */
  const VOWEL = 0;

  /**
   * Used internally to identify consonants
   * @var integer
   */
  const CONSONANT = 1;

  /**
   * Generate a phonemic password with at least $min_syllables and at most $max_syllables syllables
   * @param integer $min_syllables The number of syllables used to generate a password at least
   * @param integer $max_syllables The number of syllables used to generate a password at most
   * @param integer $flax One of SecureHashing::DIGIT, SecureHashing::UPPER, SecureHashing::EXTRA. Can be or'ed (|) together, e.g. SecureHashing::DIGIT|SecureHashing::UPPER|SecureHashing::EXTRA
   * @return Array of the password and the pronounciation (in this order)
   */
  public function phonemic($min_syllables=8,$max_syllables=8,$flags=0) {
    static $consonants = array('w', 'r', 't', 'p', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'qu', 'y','sch','nn','mm','ff','pl', 'pr', 'ps', 'bl', 'br', 'bs', 'gl', 'gr', 'kl', 'kr', 'cl', 'cr', 'dr', 'tr', 'vr', 'fr', 'st', 'sp', 'sk');
    static $vowels = array('a', 'o', 'e', 'i', 'u','eu','au','ei');
    static $extra_chars = array('(',')','{','}','/','!','"','$','%','&','=','?','+','*','#');
    static $digits = array('0','1','2','3','4','5','6','7','8','9');

    $len = self::$rng_provider->get_random_num($min_syllables,$max_syllables);
    $num_sylls = 0;
    $pw = "";
    $hyphened = "";
    $last_syll = self::VOWEL;
    $had_breaks = 0;

    # we want:
    #  - when specified DIGIT and UPPER and EXTRA:
    #    $len / 3 - 3 times CV, DIGIT|UPPER, $len / 3 - 3 times CV, EXTRA, $len / 3 - 3 times CV, DIGIT|UPPER
    #  - when specified DIGIT and UPPER:
    #    $len / 2 - 2 times CV, DIGIT|UPPER, $len / 2 - 2 times CV, DIGIT|UPPER
    #  - when specified only EXTRA, DIGIT or UPPER:
    #    $len / 2 - 1 times CV, DIGIT/EXTRA/UPPER, $len / 2 - 1 times CV
    #  - when specified DIGIT or UPPER with EXTRA:
    #    $len / 2 - 2 times CV, EXTRA, $len / 2 - 2 times CV, DIGIT/UPPER

    $breaks = 0;
    foreach(Array(self::DIGIT,self::UPPER,self::EXTRA) as $f) {
      if($flags & $f) ++$breaks;
    }

    do {
      if($last_syll == self::VOWEL) { /* we want a consonant or an extra */
        if($breaks > 0 && $num_sylls >= $had_breaks * (int)($len / $breaks)) {
          if($flags & self::DIGIT && $flags & self::UPPER) {
            if(self::$rng_provider->get_random_num(1,2) == 2) {
              $char = $digits[self::$rng_provider->get_random_num(0,count($digits)-1)];

              $hyphened .= '-'.$char.'-';
              $pw .= $char;

              $flags = $flags & ~self::DIGIT;
            }
            else {
              $char = "";
              if(self::$rng_provider->get_random_num(1,2) == 2) $char = ucfirst($consonants[self::$rng_provider->get_random_num(0,count($consonants)-1)]);
              else $char = ucfirst($vowels[self::$rng_provider->get_random_num(0,count($vowels)-1)]);

              $hyphened .= '-'.$char;
              $pw .= $char;

              $flags = $flags & ~self::UPPER;
            }
          }
          elseif($flags & self::EXTRA) {
            $char = $extra_chars[self::$rng_provider->get_random_num(0,count($extra_chars)-1)];

            $pw .= $char;
            $hyphened .= '-'.$char.'-';

            $flags = $flags & ~self::EXTRA;
          }
          elseif($flags & self::DIGIT) {
            $char = $digits[self::$rng_provider->get_random_num(0,count($digits)-1)];

            $pw .= $char;
            $hyphened .= '-'.$char.'-';

            $flags = $flags & ~self::DIGIT;
          }
          else {
            if(self::$rng_provider->get_random_num(1,2) == 2) $char = ucfirst($consonants[self::$rng_provider->get_random_num(0,count($consonants)-1)]);
            else $char = ucfirst($vowels[self::$rng_provider->get_random_num(0,count($vowels)-1)]);

            $pw .= $char;
            $hyphened .= '-'.$char;

            $flags = $flags & ~self::UPPER;
          }

          $last_syll = self::VOWEL;
          ++$had_breaks;
        }
        else {
          $char = $consonants[self::$rng_provider->get_random_num(0,count($consonants)-1)];

          $pw .= $char;
          $hyphened .= $char;

          $last_syll = self::CONSONANT;
        }
      }
      else {
        $char = $vowels[self::$rng_provider->get_random_num(0,count($vowels)-1)];

        $pw .= $char;
        $hyphened .= $char;

        $last_syll = self::VOWEL;
      }
    } while(++$num_sylls < $len);

    if($hyphened[0] == '-') $hyphened[0] = "";
    $hyphened = str_replace('--','-',$hyphened);

    return Array($pw,$hyphened);
  }

}

SecureHashing::set_default_hash_provider(SecureHashing::register_hash_provider(new SecureHashing_HashProvider_SHA512()));
SecureHashing::set_rng_provider(new SecureHashing_DefaultRNGProvider());

/*$hasher = new SecureHashing();
for($i=0;$i<10;++$i) {
  $x = $hasher->phonemic(8,8,SecureHashing::DIGIT|SecureHashing::EXTRA|SecureHashing::UPPER);
  echo $x[0],"\t\t",$x[1],"\n";
}*/

# eof
?>